﻿/**
 * @ngdoc overview
 * @name  ibosApp.services
 * @description
 *   通用服务
 */

angular.module('ibosApp.services', [])

.constant('REGEXP_ENUM', {
  notempty: '\\S+', //非空
  chinese: '^[\\u4E00-\\u9FA5\\uF900-\\uFA2D]+$', //仅中文
  letter: '^[A-Za-z]+$', //字母
  num: '^([+-]?)\\d*\\.?\\d+$', //数字
  idcard: '^[1-9]([0-9]{14}|[0-9]{17})$', //身份证
  mobile: '^1\\d{10}$', //手机
  money: '^(-)?(([1-9]{1}\d*)|([0]{1}))(\.(\d){1,2})?$', //精确到小数点两位的金额格式
  tel: '^(([0\\+]\\d{2,3}-)?(0\\d{2,3})-)?(\\d{7,8})(-(\\d{3,}))?$', //电话号码的函数(包括验证国内区号,国际区号,分机号)
  zipcode: '^\\d{6}$', //邮编
  email: '^\\w+((-\\w+)|(\\.\\w+))*\\@[A-Za-z0-9]+((\\.|-)[A-Za-z0-9]+)*\\.[A-Za-z0-9]+$', //邮件
})

.constant('API_SERVER', 'https://api.ibos.cn/v1')

.provider('Resolvers', function() {
  var resolvers = {
    qqmap: ['$ocLazyLoad', function($ocLazyLoad) {
      return $ocLazyLoad.load('components/qqmap/qqmap.js')
    }],

    canvasPainter: ['$ocLazyLoad', function($ocLazyLoad) {
      return $ocLazyLoad.load('components/angular-canvas-painter/angular-canvas-painter.min.js')
    }],
  }

  return {
    get: function(nameArr) {
      var ret = {}
      if(nameArr && nameArr.length) {
        angular.forEach(nameArr, function(name) {
          ret[name] = resolvers[name]
        })
      }
      return ret
    },

    $get: function() {
      return {}
    }
  }
})

/**
 * @ngdoc service
 * @name ibosApp.services.Settings
 * @todo 企业令牌放在此服务不合适
 *
 * @property {String}  appName    程序名
 * @property {String}  corptoken  企业令牌
 * @property {String}  hostUrl    服务器 URL
 * @property {String}  rootUrl    接口基本 URL
 * @property {String}  mainState  初始状态state
 *
 * @description
 *   程序基础设置
 */
.factory('Settings', function($location, $window, $ionicPopup, Utils) {
  var appName = '微办公';
  var searchParams = Utils.parseSearch($window.location.search);

  function getOrigin() {
    var result = $window.location.href.match(/(^.*\/)mobile/);
    return result ? result[1] : '';
  }

  var hostUrl = searchParams && searchParams.host ?
    searchParams.host.replace(/\/*$/, '/') :getOrigin();
    if(hostUrl==''){
        hostUrl = getCookieByKey('oa_h5_url');
    }

  var rootUrl = hostUrl + '?r=mobile';

  var corptoken = window.corptoken ? window.corptoken :
    ($location.search().corptoken ?
      $location.search().corptoken :
      searchParams && searchParams.corptoken || '');

  var mainState = 'portal';

  function getCookieByKey(key) {
      var cookies = document.cookie.split('; '), url = '';
      for(var i=0; i < cookies.length; i++) {
          var values = cookies[i].split('=')
          if (values[0] === key) {
              url = values[1]
              break;
          }
      }
      return url
  }
  /**
   * @ngdoc     method
   * @name      ibosApp.services.Settings#requestError
   * @methodOf  ibosApp.services.Settings
   * @description
   *    jsonp 出现错误弹出提示消息，由于 $ionicPopup 是一个promise
   *    可以使用相关功能进行后续处理
   *    参数是一个 HttpPromise 的返回结果
   *
   * @returns   {Promise}    Promise
   */
  function requestError(data, status, headers, config) {
    data = data || {};
    config = config || {};

    var errorMsg = (status == 0 && data == null) ? 'CORS 跨域请求失败，请参考 <a href="http://doc.ibos.com.cn/article/detail/id/268">http://doc.ibos.com.cn/article/detail/id/268</a> 配置服务器' : (typeof data == 'string' ? data : angular.toJson(data))

    return $ionicPopup.alert({
      title: '服务器连接失败',
      template: '<strong>url</strong>: ' + config.url +
        '<br> <strong>status</strong>: ' + status +
        '<br> <strong>msg</strong>: ' + errorMsg
    });
  }

  return {
    appName: appName,
    corptoken: corptoken,
    hostUrl: hostUrl,
    rootUrl: rootUrl,
    mainState: mainState,
    requestError: requestError
  };
})

/**
 * @ngdoc service
 * @name ibosApp.services.Utils
 * @description
 *   一些工具函数
 */
.factory('Utils', function($http, $q, $window, $timeout, $state, $ionicHistory, $ionicScrollDelegate, $ionicViewSwitcher, IbPopup) {
  function isFile(obj) {
    return toString.call(obj) === '[object File]';
  }

  function isBlob(obj) {
    return toString.call(obj) === '[object Blob]';
  }

  var ret = {
    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#isFormTypeSupported
     * @methodOf  ibosApp.services.Utils
     * @description
     *    表单支持类型检测
     * @param   {String}    type   表单控件类型
     * @returns {Boolean}   支持情况
     */
    isFormTypeSupported: function(type) {
      var elem = document.createElement("input");
      elem.setAttribute("type", type);
      return elem.type == type;
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#parseSearch
     * @methodOf  ibosApp.services.Utils
     * @description
     *    将url查询参数解析成对象
     *
     * @param   {String}   search   url查询参数
     * @returns {Object}   参数对象
     */
    parseSearch: function(search) {
      search = decodeURIComponent(search).replace(/^\?/, '');
      var searchField = search.split('&');
      var ret = {};

      for (var i = 0; i < searchField.length; i++) {
        var kt = searchField[i].split('=');
        ret[kt[0]] = kt[1]
      }

      return ret;
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#capitalize
     * @methodOf  ibosApp.services.Utils
     * @description
     *    把字符串转化首字母大写
     *
     * @param   {String} str  要操作的字符串
     * @returns {String}      首字母大写的字符串
     */
    capitalize: function(str) {
      str = str == null ? '' : String(str);
      return str.charAt(0).toUpperCase() + str.slice(1);
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#commasInclude
     * @methodOf  ibosApp.services.Utils
     * @description
     *    判断格式如“1,2,3,4”的字符串中，是否包含某个值
     *
     * @param   {String} str  以“,”分隔的字符串
     * @param   {String} key  要检查的值
     * @returns {Boolean}     是否包含
     */
    commasInclude: function(str, key) {
      return str.split(',').indexOf(key) !== -1;
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#pick
     * @methodOf  ibosApp.services.Utils
     * @description
     *    从对象中获取需要的属性组合成新对象
     *
     * @param   {Object}   obj    源对象
     * @param   {Array}    keys   属性名数组
     *
     * @returns {Object}   由指定属性组成的新对象
     */
    pick: function(obj, keys) {
      var ret = {};
      if (!obj || !keys) {
        return obj;
      } else {
        angular.forEach(keys, function(key) {
          if (obj.hasOwnProperty(key)) {
            ret[key] = obj[key];
          }
        });
      }
      return ret;
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#eliminate
     * @methodOf  ibosApp.services.Utils
     * @todo      支持剔除多个相同值
     * @description
     *    从数组中剔除指定元素，此方法直接操作源数组，且只会剔除查找到的第一个值
     *
     * @param   {Array}     arr    要操作的数组
     * @param   {*}         item    要剔除的值
     * @returns {Number}    被剔除值的索引
     */
    eliminate: function(arr, item) {
      var index = arr.indexOf(item);
      if (index !== -1) {
        arr.splice(index, 1);
      }
      return index;
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#toggle
     * @methodOf  ibosApp.services.Utils
     * @description
     *    数组里有目标项时，剔除该项，没有则增加
     *
     * @param   {Array}     arr    要操作的数组
     * @param   {*}         item   目标项
     * @returns {Array}     源数组
     */
    toggle: function(arr, item) {
      var idx = arr.indexOf(item);

      if (idx > -1) {
        arr.splice(idx, 1);
      } else {
        arr.push(item);
      }

      return arr;
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#concatTo
     * @methodOf  ibosApp.services.Utils
     * @description
     *    合并目标数组到源数组，这里会直接修改源数组
     *
     * @param   {Array}     arr1    源数组
     * @param   {Array}     arr2    目标数组
     * @param   {Boolean}   unique  是否不添加源数组中已有的元素
     * @returns {Array}     源数组
     */
    concatTo: function(arr1, arr2, unique) {
      angular.forEach(arr2, function(val) {
        if (unique && arr1.indexOf(val) !== -1) {
          return;
        }
        arr1.push(val);
      });
      return arr1;
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#removeFrom
     * @methodOf  ibosApp.services.Utils
     * @description
     *    剔除目标数组与源数组相同的元素，这里会直接修改源数组
     *
     * @param   {Array}     arr1    源数组
     * @param   {Array}     arr2    目标数组
     * @returns {Array}     源数组
     */
    removeFrom: function(arr1, arr2) {
      angular.forEach(arr2, function(val) {
        var idx = arr1.indexOf(val);
        if (idx !== -1) {
          arr1.splice(idx, 1);
        }
      });
      return arr1;
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#promptNoPermission
     * @methodOf  ibosApp.services.Utils
     * @description
     *    提示无权限使用模块并返回门户页
     */
    promptNoPermission: function() {
      IbPopup.alert({
        title: "权限错误,请检查您有无相应的权限进行操作"
      }).then(function() {
        $ionicViewSwitcher.nextDirection('back')
        $state.go('portal')
      })
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#containsArray
     * @methodOf  ibosApp.services.Utils
     * @description
     *    判断源数组是否包含目标数组
     *
     * @param   {Array}     arr1    源数组
     * @param   {Array}     arr2    目标数组
     * @returns {Boolean}   源数组是否包含目标数组
     */
    containsArray: function(arr1, arr2) {
      for (var i = 0; i < arr2.length; i++) {
        if (arr1.indexOf(arr2[i]) === -1) {
          return false;
        }
      }
      return true;
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#getCharLength
     * @methodOf  ibosApp.services.Utils
     * @description
     *    获取字符串长度
     *    计算方式为字母长度为0.5，中文长度为1，对url会做特殊处理
     *
     * @param   {String}   str   要计算长度的字符串
     * @returns {Number}         长度
     */
    getCharLength: (function() {
      var byteLength = function(str) {
        if (typeof str == "undefined") {
          return 0
        }

        var sChar = str.match(/[^\x00-\x80]/g);
        // 匹配出中文字符的数目并再次加上，这样子每个中文字符占位为2
        return (str.length + (!sChar ? 0 : sChar.length))
      };

      return function(str, opt) {
        opt = opt || {};
        opt.max = opt.max || 140;
        // opt.min = opt.min || 41;
        opt.surl = opt.surl || 20;
        var p = str.trim().length;
        if (p > 0) {
          // 下面这段正则用于匹配URL
          var result = str.match(/(http|https):\/\/[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)+([-A-Z0-9a-z\$\.\+\!\_\*\(\)\/\,\:;@&=\?~#%]*)*/gi) || [];
          var total = 0;
          for (var m = 0,
              len = result.length; m < len; m++) {
            var o = byteLength(result[m]);

            total += o <= opt.max ? opt.surl : (o - opt.max + opt.surl)

            str = str.replace(result[m], "")
          }
          return Math.ceil((total + byteLength(str)) / 2)
        } else {
          return 0
        }
      }
    })(),

    serverWarning: function(res) {
    },

    serverError: function(res) {
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#postJsonData
     * @methodOf  ibosApp.services.Utils
     * @todo：(ノ=Д=)ノ┻━┻  现在又说要改回默认用 json
     * @description
     *    使用 json 格式提交表单
     *
     *    本来 angular 默认是使用 json 格式提交数据的
     *    但由于之前后端规范使用了 FormData，所以默认修改成了 json 格式
     *    此方法用于临时需要使用 json 格式的情形
     *
     * @param     {String}    url         要提交的地址
     * @param     {Object}    data        要提交的数据
     * @param     {Object}    httpConf    ajax配置
     * @returns   {Promise}   Promise
     */
    postJsonData: function(url, data, httpConf) {
      return $http(angular.extend({
        method: 'POST',
        url: url,
        // headers: { 'Content-Type': undefined },
        headers: {
          'Content-type': 'application/json'
        },
        data: data,
        transformRequest: [function(d) {
          return angular.isObject(d) && !isFile(d) && !isBlob(d) ? angular.toJson(d) : d;
        }]
      }, httpConf));
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#postFormData
     * @methodOf  ibosApp.services.Utils
     * @description
     * 提交 FormData 类型的数据
     *
     * @param     {String}    url         提交地址
     * @param     {FormData}  formData    FormData 类型的数据
     * @returns   {Promise}   Promise
     */
    postFormData: function(url, formData, config) {
      // 此处由于使用了 FormData 对象提交，需要配置 header 跟 transformRequest 方法
      return $http(angular.extend({
        method: 'POST',
        url: url,
        headers: {
          'Content-Type': undefined
        },
        data: formData,
        transformRequest: angular.identity
      }, config));
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#getImagePreviewUrl
     * @methodOf  ibosApp.services.Utils
     * @todo      迁移至 file-util 模块
     * @description
     * 获取图片预览地址，先尝试创建 Blob 地址，没有时使用 fileReader
     *
     * @param     {File|Blob}      file       File 对象或 Blob 对象
     * @param     {Function}       callback   回调函数
     * @returns   {FileReader}     FileReader 对象
     */
    getImagePreviewUrl: function(file, callback) {
      if (!angular.isFunction(callback)) return;
      if ($window.URL) {
        callback($window.URL.createObjectURL(file));
      } else if ($window.webkitURL) {
        callback($window.webkitURL.createObjectURL(file));
      } else {
        this.readFile(file, function(evt) {
          callback(evt.target.result);
        })
      }
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#setDocumentTitle
     * @methodOf  ibosApp.services.Utils
     * @description
     *   设置文档标题
     *   微信下 iOS 的 document title 在页面加载完成后再设置不起效
     *   于是有了这个 hack 方法
     *
     * @param     {String}    title    文档标题
     */
    setDocumentTitle: function(title) {
      document.title = title;

      if (ionic.Platform.isIOS()) {
        var $iframe = angular.element('<iframe src="/favicon.ico" style="display: none;"></iframe>').on('load', function() {
          setTimeout(function() {
            $iframe.off('load').remove();
          }, 0);
        });
        angular.element(document.body).append($iframe);
      }
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#httpPromise
     * @methodOf  ibosApp.services.Utils
     * @description
     *   angular 的 http 请求继承于 $q.defer，但比普通的defer.promise 多了success和error方法
     *     此方法将普通的 defer.promise 封装类似 http 返回的 promise
     *
     * @param     {Promise}      promise    promise对象
     * @returns   {HttpPromise}  HttpPromise
     */
    httpPromise: function(promise) {
      promise.success = function(fn) {
        promise.then(fn);
        return promise;
      };

      promise.error = function(fn) {
        promise.then(null, fn);
        return promise;
      };
      return promise;
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#disableBack
     * @methodOf  ibosApp.services.Utils
     * @deprecated
     * @description
     *   禁止下一个视图的后退功能（即不允许下一个视图返回当前这个视图）
     *
     * @param     {Object}   options   视图选项
     */
    disableBack: function(options) {
      return $ionicHistory.nextViewOptions(angular.extend({
        disableBack: true
      }, options));
    },

    triggerScrollViewPullToRefresh: function(scrollView) {
      $timeout(function() {
        scrollView.__publish(
          scrollView.__scrollLeft, -scrollView.__refreshHeight,
          scrollView.__zoomLevel, true);

        var d = new Date();

        scrollView.refreshStartTime = d.getTime();

        scrollView.__refreshActive = true;
        scrollView.__refreshHidden = false;
        if (scrollView.__refreshShow) {
          scrollView.__refreshShow();
        }
        if (scrollView.__refreshActivate) {
          scrollView.__refreshActivate();
        }
        if (scrollView.__refreshStart) {
          scrollView.__refreshStart();
        }
      });
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#triggerScrollViewPullToRefreshByHandle
     * @methodOf  ibosApp.services.Utils
     * @description
     *    手动触发面板的下拉刷新功能
     *
     * @param     {String}      delegateHandle    指定某个具体的滚动对象
     */
    triggerScrollViewPullToRefreshByHandle: function(delegateHandle) {
      var scrollView = $ionicScrollDelegate.$getByHandle(delegateHandle).getScrollView();
      if (!scrollView) return;
      this.triggerScrollViewPullToRefresh(scrollView);
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#clearStateCacheAndGo
     * @methodOf  ibosApp.services.Utils
     * @description
     *    清除指定 state 的缓存及跳转至该 state
     *
     * @param     {String}      state    页面 state
     * @returns   {Promise}     Promise
     */
    clearStateCacheAndGo: function(state) {
      return $ionicHistory.clearCache([state]).then(function() {
        $state.go(state);
      });
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.Utils#checkFormDirtyBeforeLeave
     * @methodOf  ibosApp.services.Utils
     * @param     {Object} $scope       Scope
     * @param     {String} formCtrlKey  表单控制器字段
     * @param     {String} tip          提示文本
     */
    checkFormDirtyBeforeLeave: function($scope, formCtrlKey, tip) {
      var off
      tip = tip || '还有未保留的修改，是否确定离开'
      off = $scope.$on('$stateChangeStart', function(e, to, toParams) {
        var formCtrl = $scope[formCtrlKey]

        // 表单发生了修改时
        if(formCtrl && formCtrl.$dirty) {
          e.preventDefault()
          // 后退时历史记录会少一，这里加一个以保持历史纪录正确
          $window.history.pushState(null, '', $window.location.href)
    
          IbPopup.confirm({
            title: '提示',
            content: tip
          })
          .then(function(res) {
            if(res) {
              off()
              $window.history.back()
            }
          })
        }
      })
    }
  };

  /**
   * @ngdoc     method
   * @name      ibosApp.services.Utils#isDateTimeLocalSupport
   * @methodOf  ibosApp.services.Utils
   * @description
   *    是否支持 input[type="datetime"]
   *
   * @returns {boolean}   支持情况
   */
  ret.isDateTimeLocalSupport = ret.isFormTypeSupported('datetime-local');

  return ret;
})

/**
 * @ngdoc service
 * @name ibosApp.services.$localstorage
 * @description
 *   本地储存操作
 */
.factory('$localstorage', ['$window', function($window) {
  if ($window.localStorage) {
    return {
      /**
       * @ngdoc     method
       * @name      ibosApp.services.$localstorage#set
       * @methodOf  ibosApp.services.$localstorage
       * @description
       *    写入本地存储，针对基本类型
       *
       * @param   {String}    key      键名
       * @param   {*}         value    键值
       */
      set: function(key, value) {
        $window.localStorage.setItem(key, value);
      },

      /**
       * @ngdoc     method
       * @name      ibosApp.services.$localstorage#get
       * @methodOf  ibosApp.services.$localstorage
       * @description
       *    获取指定本地存储
       *
       * @param   {String}    key            键名
       * @param   {*}         defaultValue   无结果时的默认值
       * @returns {*}         对应键名的值
       */
      get: function(key, defaultValue) {
        return $window.localStorage.getItem(key) || defaultValue;
      },

      /**
       * @ngdoc     method
       * @name      ibosApp.services.$localstorage#remove
       * @methodOf  ibosApp.services.$localstorage
       * @description
       *    删除指定本地存储
       *
       * @param   {String}    key            键名
       */
      remove: function(key) {
        $window.localStorage.removeItem(key);
      },

      /**
       * @ngdoc     method
       * @name      ibosApp.services.$localstorage#setObject
       * @methodOf  ibosApp.services.$localstorage
       * @description
       *    写入本地存储，针对引入类型，会先转为JSON
       *
       * @param   {String}    key      键名
       * @param   {*}         value    键值
       */
      setObject: function(key, value) {
        $window.localStorage.setItem(key, JSON.stringify(value));
      },

      /**
       * @ngdoc     method
       * @name      ibosApp.services.$localstorage#getObject
       * @methodOf  ibosApp.services.$localstorage
       * @description
       *    获取指定本地存储，针对引入类型，获取后会从 JSON 转为对象
       *
       * @param   {String}    key      键名
       * @returns {*}         经过JSON解析的对应键名的值
       */
      getObject: function(key) {
        return JSON.parse($window.localStorage.getItem(key));
      }
    };
  } else {
    return {
      set: angular.noop,
      get: function(key, defaultValue) {
        return defaultValue;
      },
      remove: angular.noop,
      setObject: angular.noop,
      getObject: angular.noop
    };
  }
}])

/**
 * @ngdoc service
 * @name ibosApp.services.TalkingData
 * @description
 *   TalkingData 统计
 */
.factory('TalkingData', function(User, Settings) {

  return {
    /**
     * @ngdoc     method
     * @name      ibosApp.services.TalkingData#onEvent
     * @methodOf  ibosApp.services.TalkingData
     * @description
     *   触发统计事件
     *
     * @param     {String}  evtId     事件标识
     * @returns   {String}  [label]   标签
     * @returns   {Object}  [mapKv]   其他参数
     */
    onEvent: function(evtId, label, mapKv) {
      if (typeof TDAPP !== 'undefined') {

        mapKv = angular.extend({}, mapKv, {
          uid: User.uid,
          hostUrl: Settings.hostUrl
        });

        return TDAPP.onEvent(evtId, label, mapKv);
      }
    }
  };
})

// App 数据
.factory('Apps', function() {

  var apps = [];

  return {
    install: function(conf) {
      apps.push(conf);
    },

    all: function() {
      return apps;
    }
  };
})

/**
 * @ngdoc service
 * @name ibosApp.services.User
 * @description
 *   用户数据服务
 */
.provider('User', function() {

  var userResolve = [
    '$rootScope',
    '$state',
    '$ionicModal',
    'TalkingData',
    'Settings',
    'User',
    'IbPopup',
    'API_SERVER',

    function($rootScope, $state, $ionicModal, TalkingData, Settings, User, IbPopup, API_SERVER) {
      if (!User.isLogin) {
          var oaurlKey = 'oa_h5_url',
              oausername = 'oa_h5_username',
              oauserpass = 'oa_h5_userpass'
          data = {
              oaurl: getCookieByKey(oaurlKey),
              username: getCookieByKey(oausername),
              password: getCookieByKey(oauserpass)
          };

          var _promise = User.login(data)
          .success(function(res) {
            if (res.login) {
              TalkingData.onEvent('登录成功', '', {
                hostUrl: Settings.hostUrl
              });
            } else {
              TalkingData.onEvent('登录失败', '', {
                hostUrl: Settings.hostUrl,
                res: res
              });
            }
          })
          .error(function(res) {
            console.log(res)
            TalkingData.onEvent('登录出错', '', {
              res: res,
              hostUrl: Settings.hostUrl
            });
            $state.go('login');
          })
        return _promise;
      }
      function getCookieByKey(key) {
          var cookies = document.cookie.split('; '), ret = '';
          for(var i=0; i < cookies.length; i++) {
              var values = cookies[i].split('=')
              if (values[0] === key) {
                  ret = values[1]
                  break;
              }
          }
          return ret
      }
    }
  ];

  return {
    // 所有数据的加载都要在用户登录之后才执行
    resolve: {
      user: userResolve
    },

    $get: function($http, $state, $rootScope, $q, $window, $ionicPopup, Settings, IbPopup, Utils, API_SERVER) {
      var ret = {
        uid: '',
        user: {},
        users: {},
        group: {},
        isLogin: false
      };

      function loginFailed(res) {
        $ionicPopup.alert({
          title: '登录失败',
          template: res.msg || '未知错误'
        });
        $state.go('login');
      }

      // 登录
      function getUserInfo(loginData) {
        var promise = $http.get(Settings.rootUrl + '/default/login&issetuser=true', {
          params: loginData,
          responseType: "json"
        })

        .success(function(res) {
          if (res.login) {
            ret.uid = res.uid;
            ret.user = res.user;

            ret.formhash = res.formhash;
            ret.isLogin = true;

            // 用户、部门、岗位数据，分别带有字母分组信息
            // ret.userData = res.userData;
            ret.departmentData = res.departmentData;
            ret.positionData = res.positionData;

            // 延迟加载用户数据
            $http.get(Settings.rootUrl + '/default/login')
            .success(function(res) {
              ret.userData = res.userData;
              angular.extend(ret.users, res.userData.datas)
              angular.extend(ret.group, res.userData.group)
            })

            // 将用户信息绑定到 $rootScope，方便后续使用
            $rootScope.user = res.user;
          } else {
            loginFailed(res);
          }
        })

        .error(Settings.requestError)

        return promise;
      }

      function getServerInfo(corptoken) {
        return $http.jsonp(API_SERVER + '/oauth/connect', {
          params: {
            corptoken: corptoken,
            callback: 'JSON_CALLBACK'
          }
        });
      }

      function getRedirectUri() {
        var redirect;
        if (/[?&]corptoken=/.test($window.location.search)) {
          redirect = $window.location.href;
        } else {
          var pathArr = $window.location.href.split(/(?=#)/);

          pathArr.splice(1, 0, (pathArr[0].indexOf('?') !== -1 ? '&' : '?') + 'corptoken=' + Settings.corptoken);
          redirect = pathArr.join('');
        }
        return redirect;
      }

      // 以 corptoken 方式登录
      function loginByCorptoken(_corptoken) {

        var defer = $q.defer();

        getServerInfo(_corptoken)
          .success(function(res) {
            if (res.code === 0) {
              Settings.hostUrl = res.data.systemurl + (res.data.systemurl.charAt(res.data.systemurl.length - 1) === '/' ? '' : '/');
              Settings.rootUrl = Settings.hostUrl + '?r=mobile';

              delete res.data.systemurl;

              var promise = $http.get(Settings.hostUrl + "api/co/oauth/", {
                  params: res.data,
                  responseType: "json"
                })
                .success(function(res) {
                  if (res.isSuccess) {
                    getUserInfo().success(function(res) {
                        defer.resolve(res);
                      })
                      .error(function(res) {
                        defer.reject(res);
                      });
                  } else {
                    IbPopup.alert({
                      title: '请先绑定IBOS账号！'
                    }).then(function() {
                      $window.location.href = API_SERVER + '/oauth/connect?corptoken=' + _corptoken + '&redirect_uri=' + getRedirectUri();
                    });
                  }
                })
                .error(Settings.requestError)

              return promise;
            } else {
              alert(res.message || '登录失败：corptoken: ' + _corptoken);
            }
          });

        return Utils.httpPromise(defer.promise);
      }

      function setDocumentTitleWithCorpName(res) {
        if (res.login && res.unit) {
          Utils.setDocumentTitle(res.unit.shortname);
        }
      }

      /**
       * @ngdoc     method
       * @name      ibosApp.services.User#login
       * @methodOf  ibosApp.services.User
       * @description
       *    登录用户
       *
       * @param     {Object}      loginData     登录用数据
       * @returns   {HttpPromise} HttpPromise
       */
      ret.login = function(loginData) {
        if (Settings.corptoken) {
          return loginByCorptoken(Settings.corptoken).success(setDocumentTitleWithCorpName);
        } else if (Settings.hostUrl) {
          return getUserInfo(loginData).success(setDocumentTitleWithCorpName)
        } else {
          return Utils.httpPromise($q.reject());
        }
      };

      /**
       * @ngdoc     method
       * @name      ibosApp.services.User#logout
       * @methodOf  ibosApp.services.User
       * @description
       *    登出用户
       * @returns   {HttpPromise}  HttpPromise
       */
      ret.logout = function() {
        return $http.get(Settings.rootUrl + '/default/logout')
          .success(function() {
            $state.go('login');
          });
      };

      /**
       * @ngdoc     method
       * @name       services.#authority
       * @methodOf  services.
       * @param；
       *
       * @description
       *   用户是否有权限使用模块
       *
       * @returns   {HttpPromise}   HttpPromise
       */
      ret.getAuthority = function() {
        return $http.get(Settings.hostUrl + '?r=mobile/authority/index')
      };

      /**
       * @ngdoc     method
       * @name      ibosApp.services.User#getUser
       * @methodOf  ibosApp.services.User
       * @description
       *    获取单个用户的数据
       *
       * @param     {String}      uid     用户id
       * @returns   {Object}      用户数据
       */
      ret.getUser = function(uid) {
        return ret.users[uid || ret.uid] || null;
      };

      /**
       * @ngdoc method
       * @name      ibosApp.services.User#groupUidByLetter
       * @methodOf  ibosApp.services.User
       * @todo 太绕太浪费性能了，有必要修改数据源格式
       * @description
       *   将 uid 按字母索引分组
       *
       * @param   {Array}    uids    用户数组 id
       * @returns {Object}     用户分组
       */
      ret.groupUidByLetter = function(uids) {
        var ret = {};
        var group = this.group;

        angular.forEach(group, function(_uids, letter) {
          angular.forEach(_uids, function(_uid) {
            if (uids.indexOf(+_uid) !== -1 || uids.indexOf('' + _uid) !== -1) {
              ret[letter] = ret[letter] || [];
              ret[letter].push(_uid);
            }
          });
        });

        return ret;
      };

      function $$addPrefix(id, prefix) {
        if (!id) {
          return id;
        }
        return (prefix + id).replace(/,/g, ',' + prefix);
      }

      function $$removePrefix(id, prefix) {
        if (angular.isArray(id)) {
          return id.map(function(_id) {
            return _id.replace(new RegExp('^' + prefix), '');
          });
        }

        if (angular.isString(id)) {
          var reg = new RegExp(',' + prefix, 'g');
          return id.replace(reg, ',').slice(2);
        }

        return id;
      }
      /**
       * @ngdoc method
       * @name      ibosApp.services.User#addPrefix
       * @methodOf  ibosApp.services.User
       * @description
       *   给 uid 添加 u_ 前缀
       *   某些情况下为了与后端数据一致会需要用到
       *
       * @param   {String}    [uid]    单个 uid 或以 ',' 作为分隔符的一组 uid
       * @returns {String}             形如 'u_1,u_2...' 的字符串
       */

      /**
       * @ngdoc method
       * @name      ibosApp.services.User#removePrefix
       * @methodOf  ibosApp.services.User
       * @description
       *   去掉 uid 的 u_ 前缀
       *
       * @param   {String}    [uid]    有前缀的 uid
       * @returns {String}    形如 '1,2...' 的字符串
       */
      angular.forEach({
        'Prefix': 'u_',
        'PositionPrefix': 'p_',
        'DepartmentPrefix': 'd_'
      }, function(prefx, key) {
        ret['add' + key] = function(id) {
          return $$addPrefix(id, prefx);
        };
        ret['remove' + key] = function(id) {
          return $$removePrefix(id, prefx);
        };
      });

      return ret;
    }
  };
})

/**
 * @ngdoc service
 * @name ibosApp.services.Department
 * @description
 *   部门数据服务
 */
.factory('Department', function(Utils, User) {

  var Department = {
    getDepartments: function() {
      return User.departmentData.datas;
    },

    /**
     * @ngdoc method
     * @name      ibosApp.services.Corp#getSubDepartments
     * @methodOf  ibosApp.services.Corp
     * @description
     *   获取指定部门的子部门，若不传参数，则获取第一级部门
     *
     * @param   {String}    [deptId='0']   部门 id
     * @returns {Array}     子部门数组
     */
    getSubDepartments: function(deptId) {
      var depts = [];
      var departments = this.getDepartments();

      deptId = deptId || '0';
      angular.forEach(departments, function(dept) {
        if (dept.pid == deptId) {
          depts.push(dept);
        }
      });
      return depts;
    },

    /**
     * @ngdoc method
     * @name      ibosApp.services.Department#getUserByDepartment
     * @methodOf  ibosApp.services.Department
     * @description
     *   获取指定部门的用户
     *
     * @param   {String}    [deptId='0']   部门 id
     * @returns {Array}     用户数组
     */
    getUserByDepartment: function(deptId) {
      var ret = [];
      var users = User.userData.datas;

      deptId = deptId || '0';
      angular.forEach(users, function(user) {
        // 由于用户可能同时属于多个部门，此处判断是否包含目标部门
        if (Utils.commasInclude(user.deptid, deptId)) {
          ret.push(user);
        }
      });

      return ret;
    },

    /**
     * @ngdoc method
     * @name      ibosApp.services.Department#getUidGroupByDepartment
     * @methodOf  ibosApp.services.Department
     * @description
     *   获取指定部门的用户分组
     *
     * @param   {String}    [deptId='0']   部门 id
     * @returns {Object}     用户分组
     */
    getUidGroupByDepartment: function(deptId) {
      var users = this.getUserByDepartment(deptId);
      var uids = users.map(function(user) {
        return user.uid
      });

      return User.groupUidByLetter(uids);
    }
  };

  return Department;
})

/**
 * @ngdoc service
 * @name ibosApp.services.Position
 * @description
 *   岗位数据服务
 */
.factory('Position', function(Utils, User) {

  function includeId(str, id) {
    return str.split(',').indexOf(id) !== -1;
  }

  return {
    getPositions: function() {
      return User.positionData.datas;
    },

    /**
     * @ngdoc method
     * @name      ibosApp.services.Position#getUserByPosition
     * @methodOf  ibosApp.services.Position
     * @description
     *   获取指定岗位的用户
     *
     * @param   {String}    [posId]   岗位 id
     * @returns {Array}     用户数组
     */
    getUserByPosition: function(posId) {
      var ret = [];
      var users = User.users;

      angular.forEach(users, function(user) {
        // 由于用户可能同时属于多个部门，此处判断是否包含目标部门
        if (Utils.commasInclude(user.positionid, posId)) {
          ret.push(user);
        }
      });

      return ret;
    },

    /**
     * @ngdoc method
     * @name      ibosApp.services.Position#getUidGroupByPosition
     * @methodOf  ibosApp.services.Position
     * @description
     *   获取指定岗位的用户分组
     *
     * @param   {String}    posId   岗位 id
     * @returns {Object}     用户分组
     */
    getUidGroupByPosition: function(posId) {
      var users = this.getUserByPosition(posId);
      var uids = users.map(function(user) {
        return user.uid
      });

      return User.groupUidByLetter(uids);
    }
  };
})

/**
 * @ngdoc service
 * @name ibosApp.services.ImgViewer
 * @description
 *   提供兼容多个平台的图片预览功能
 *   微信端调用微信提供的接口，其他（如PC端）暂时只做新窗口打开
 *
 */
.factory('ImgViewer', function(Settings, $window, $timeout, $rootScope, $ionicModal, $ionicSlideBoxDelegate) {
  var viewerModal;

  /**
   * @ngdoc     method
   * @name      ibosApp.services.ImgViewer#view
   * @methodOf  ibosApp.services.ImgViewer
   * @todo
   *   此方法对非微信端的交互需要优化
   * @description
   *   预览一组图片，目前此功能调用微信 JSSDK
   *   非微信端则直接打开在新窗口该图片
   * @param     {String}    src      当前要查看的图片的 src
   * @param     {Array}     srcs     提供一组图片的 srcs，src 参数也在此数组之中
   */
  function view(src, srcs) {
    // 微信平台
    if (typeof WeixinJSBridge !== "undefined") {
      WeixinJSBridge.invoke("imagePreview", {
        "current": src,
        "urls": srcs
      });
      // 其他情况，如 PC 端
    } else {
      // 如果已经存在查看器 Modal 实例
      if (viewerModal) {
        viewerModal.scope.urls = srcs;
        viewerModal.scope.current = srcs.indexOf(src);
        $timeout(function() {
          viewerModal.show();
        }, 50);
        // 否则，新建 Modal
      } else {
        var scope = $rootScope.$new();
        scope.urls = srcs;
        scope.current = srcs.indexOf(src);

        $ionicModal.fromTemplateUrl('templates/img-preview.html', {
          scope: scope,
          animation: 'none'
        }).then(function(modal) {
          viewerModal = modal;
          modal.show();
          modal.hideDelay = 1;
          scope.closeModal = function() {
            modal.hide();
          };
        });

        scope.$on('modal.shown', function($evt, modal) {
          var instance = $ionicSlideBoxDelegate.$getByHandle('imgPreviewSlide');
          if (instance) {
            instance.slide(modal.scope.current, -1);
          }
        });

        scope.$on('modal.hidden', function($evt, modal) {
          modal.scope.urls = [];
          modal.scope.current = 0;
        });

        scope.zoomImg = function($event) {
          $event.target.style.width = !$event.target.zoomFlag ? '100%' : 'auto';
          $event.target.zoomFlag = !$event.target.zoomFlag;
        }
      }
    }
  }

  return {
    view: view
  }
})

/**
 * @ngdoc service
 * @name ibosApp.services.FileUtils
 * @description
 *   文件相关工具方法
 */
.factory('FileUtils', function($window, $rootScope, $sce, IbPopup) {
  var officeFileTypes = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf'];
  var imageTypes = ['jpg', 'jpeg', 'png', 'bmp', 'gif'];
  var filtIconMap = {
    'zip': 'zip',
    '7z': '7z',
    'ai': 'ai',
    'exe': 'exe',
    'fla': 'fla',
    'html': 'html',
    'pdf': 'pdf',
    'psd': 'psd',
    'rar': 'rar',
    'swf': 'swf',
    'txt': 'txt',

    'php': 'code',
    'js': 'code',
    'asp': 'code',
    'aspx': 'code',
    'jsp': 'code',

    'xls': 'excel',
    'xlsx': 'excel',

    'doc': 'word',
    'docx': 'word',

    'ppt': 'ppt',
    'pptx': 'ppt',
    'pps': 'ppt',
    'ppsx': 'ppt',

    'mp3': 'music',
    'wav': 'music',
    'wma': 'music',
    'flac': 'music',
    'ape': 'music',

    'png': 'photo',
    'jpg': 'photo',
    'jpeg': 'photo',
    'gif': 'photo',

    'mp4': 'video',
    'rmvb': 'video',
    'rm': 'video',
    'avi': 'video',
    'flv': 'video',
    'mkv': 'video',
    'wmv': 'video',

    'unknown': 'unknown'
  }

  /**
   * @ngdoc     method
   * @name      ibosApp.services.FileUtils#isOfficeFile
   * @methodOf  ibosApp.services.FileUtils
   * @description
   *   判断是否是支持的文档类型
   *
   * @param     {String}    type      文件类型
   * @returns   {Boolean}   是否支持
   */
  function isOfficeFile(type) {
    type = ('' + type).toLowerCase();
    return officeFileTypes.indexOf(type) !== -1;
  }

  function isImage(type) {
    type = ('' + type).toLowerCase();
    return imageTypes.indexOf(type) !== -1;
  }

  /**
   * @ngdoc     method
   * @name      ibosApp.services.FileUtils#download
   * @methodOf  ibosApp.services.FileUtils
   * @description
   *   获取文件后缀名
   *
   * @param     {String}    url      文件地址
   * @returns   {String}    文件后缀名
   */
  function getExtname(url) {
    url = url.split('?')[0];
    var dotIndex = url.lastIndexOf('.');
    var extname = dotIndex !== -1 ? url.slice(dotIndex + 1) : url;
    return typeof extname == 'string' ? extname.toLowerCase() : '';
  }

  /**
   * @ngdoc     method
   * @name      ibosApp.services.FileUtils#getFileIconByExtname
   * @methodOf  ibosApp.services.FileUtils
   * @description
   *   根据文件后缀名获取对应的图标地址
   *
   * @param     {String}    extname      文件后缀名
   * @param     {String}    [size]       尺寸，不传为默认大小，传 'lt' 时为小图标
   * @returns   {String}    图标地址
   */
  function getFileIconByExtname(extname, size) {
    extname = typeof extname == 'string' ? extname.toLowerCase() : '';
    var pathnameOrigin = $window.location.pathname;
    var pathnameArr = pathnameOrigin.split('/');
    pathnameArr.pop();
    var safePathname = pathnameArr.concat(['']).join('/');
    return $window.location.protocol +
      '//' +
      $window.location.host +
      safePathname +
      '/img/filetype/' +
      (filtIconMap[extname] || filtIconMap.unknown) +
      (size ? '_' + size : '') + '.png';
  }

  /**
   * @ngdoc     method
   * @name      ibosApp.services.FileUtils#showDownloadTip
   * @methodOf  ibosApp.services.FileUtils
   * @description
   *   显示文件下载提示弹窗
   *
   * @param     {String}    url      文件地址
   * @returns   {String}    文件后缀名
   */
  function showDownloadTip(url) {
    var conf = {
        title: '复制地址在浏览器中访问即可下载',
        template: '<div class="selectable">' + $sce.getTrustedHtml(url) + '</div>',
      }
      // 在微信里貌似使用不了 copy 命令，目前只能让用户手动复制了
      // if($rootScope.isWeiXin) {
    return IbPopup.alert(conf);
    // } else {
    //   conf.okText = '复制地址';
    //   return IbPopup.confirm(conf).then(function(ok) {
    //     if(ok) {
    //       var selection = window.getSelection();
    //       var range = document.createRange();
    //       range.selectNodeContents(document.querySelector('.popup .selectable'));
    //       selection.removeAllRanges();
    //       selection.addRange(range);

    //       if(document.execCommand('copy'));
    //     }
    //   });
    // }
  }

  /**
   * @ngdoc     method
   * @name      ibosApp.services.FileUtils#download
   * @methodOf  ibosApp.services.FileUtils
   * @description
   *   下载文件
   *
   * @param     {String}    url      文件地址
   */
  function download(url) {
    if (!url) return;
    // 在微信中时，按文件类型处理
    if ($rootScope.isWeiXin) {
      var extname = getExtname(url);
      // 微信只支持打开下载 office 相关文档格式
      if (isOfficeFile(extname) || isImage(extname) || extname == 'html' || extname == 'htm') {
        window.location = url;
      } else {
        showDownloadTip(url);
      }
      // 其他浏览直接打开下载
    } else {
      window.location = url;
    }
  }

  return {
    isOfficeFile: isOfficeFile,
    isImage: isImage,
    getExtname: getExtname,
    showDownloadTip: showDownloadTip,
    download: download,
    getFileIconByExtname: getFileIconByExtname
  }
})

.factory('Downloader', function($window, $rootScope, IbPopup) {

  // 微信支持打开及下载的格式
  var WX_SUPPORT_FILETYPE = [
    'doc', 'docx',
    'xls', 'xlsx',
    'ppt', 'pptx',
    'pdf', 'txt',
    'png', 'gif', 'jpg', 'jpeg'
  ];
  var DOWNLOAD_PAGE = 'wxdownload.html';

  return {
    down: function(url) {
      // 微信内置浏览器有格式限制
      if ($rootScope.isWeiXin) {
        var frags = url.split('.');
        var filetype = frags[frags.length - 1];
        if (WX_SUPPORT_FILETYPE.indexOf(filetype) === -1) {
          // return IbPopup.alert({
          //   title: '复制地址在浏览器中访问即可下载',
          //   template: '<div class="selectable">' + url + '</div>'
          // });
          this.downloadInWx(url);
          return;
        }
      }
      $window.location.href = url;
    },
    downloadInWx: function(url) {
      $window.location.href = DOWNLOAD_PAGE + '?url=' + encodeURIComponent(url);
    }
  };
})

/**
 * @ngdoc service
 * @name ibosApp.services.External
 * @description
 *   外部插口调用
 */
.factory('External', function($rootScope, $window) {
  var IS_WEIXIN = $rootScope.isWeiXin;

  return {
    /**
     * @ngdoc     method
     * @name      ibosApp.services.External#getLocation
     * @methodOf  ibosApp.services.External
     * @description
     * 获取地理位置
     *   onsuccess 回调参数（一些多端不一的参数在使用时要注意）
     *   res.latitude    {Number}  纬度
     *   res.longitude   {Number}  经度
     *
     * @param     {Function}     onsuccess    成功回调
     * @param     {Function}     onsuccess    错误回调
     */
    getLocation: function(onsuccess, onerror) {
      if (IS_WEIXIN) {
        wx.ready(function() {
          wx.getLocation({
            success: function(res) {
              onsuccess(res);
            },
            fail: onerror
          });
        });

        // 使用浏览器自带的定位功能
      } else {
        if (navigator.geolocation) {
          navigator.geolocation.getCurrentPosition(function(res) {
            onsuccess({
              latitude: res.coords.latitude,
              longitude: res.coords.longitude
            });
          }, onerror);
        }
      }
    },

    openLocation: function(opts) {
      // 在微信环境下直接使用微信自带的地图
      if (IS_WEIXIN) {
        wx.openLocation(opts);

        // 否则，使用腾讯地图web/wap版
      } else {
        var url = 'http://apis.map.qq.com/uri/v1/geocoder?coord=' + opts.latitude + ',' + opts.longitude + '&referer=酷办公';
        $window.open(url);
      }
    }
  }
})

/**
 * @ngdoc service
 * @name ibosApp.services.UserSelector
 * @description
 *   人员选择控件
 */
.factory('UserSelector', function($rootScope, $q, $ionicModal, $ionicScrollDelegate, Utils, isEmptyFilter, toArrayFilter, User, Department, Position) {

  var usid = 0;

  // 根据可用 id 数组和禁用 id 数组，过滤掉不可用的 id
  function filterGroup(group, enabled, disabled) {
    var ret = {};

    // 如果配置了可用 id 数组，则不在数组内的 id 不可用
    if (enabled) {
      if (enabled.length) {
        angular.forEach(group, function(ids, letter) {
          var enabledIds = ids.filter(function(id) {
            return enabled.indexOf(+id) !== -1 || enabled.indexOf('' + id) !== -1;
          });
          if (enabledIds.length) {
            ret[letter] = enabledIds;
          }
        });
      }

      return ret;
      // 如果配置了禁用 id 数组，则不在数组内的 id 可用
    } else if (disabled) {
      if (disabled.length) {
        angular.forEach(group, function(ids, letter) {
          var enabledIds = ids.filter(function(id) {
            return disabled.indexOf(+id) === -1 || disabled.indexOf('' + id) === -1;
          });
          if (enabledIds.length) {
            ret[letter] = enabledIds;
          }
        });
      } else {
        ret = group;
      }

      return ret;

    } else {
      return group;
    }
  }

  /**
   * @ngdoc method
   * @name       ibosApp.services.UserSelector#create
   * @methodOf   ibosApp.services.UserSelector
   * @description
   *   创建人员选择器
   *
   * @param      {Obejct}   options   配置
   *    groups        {object}          分组数据
   *    tempateUrl    {String}          模板地址
   *    maxSelection  {Number}          最大可选数
   * @example
   * ```js
   * UserSelector.create().then(function(modal) {
   *   $scope.userSelectorModal = modal;
   *
   *   modal.userSelector.then(function(selected) {
   *     console.log('selected: ', selected);
   *   });
   * });
   * @returns {Promise}     Promise
   */
  function $$create(options) {

    options = options || {};

    var scope = $rootScope.$new(true);
    var promise = $ionicModal.fromTemplateUrl(options.template, {
      scope: scope,
      animation: 'slide-in-up'
    });
    var lastSelected;

    scope.usid = usid++;
    scope.selected = scope.selected || [];
    scope.departments = {};
    scope.positions = {};
    scope.groups = {};

    scope.getSelected = function() {
      return Object.keys(scope.selected).filter(function(key) {
        return scope.selected[key]
      })
    }

    // 统计已选择用户数
    scope.getSelectedCount = function() {
      if (!options.maxSelection) {
        return scope.selected.length || 0;
        // 设置了最大可选人数时
      } else {
        return (scope.selected.length || 0) + '/' + options.maxSelection;
      }
    };

    scope.setGroup = function(groups) {
      scope.groups = filterGroup(groups, options.enabled, options.disabled);
    };

    scope.toggleSelect = function(id) {
      id = '' + id;
      // 移除
      if (scope.selected.indexOf(id) !== -1) {
        Utils.eliminate(scope.selected, id);
        // 移除时，如果移除了记录着的 lastSelected，则更新 lastSelected 为选中数组的最后一个值
        if (id === lastSelected) {
          lastSelected = scope.selected[scope.selected.length - 1];
        }
        // 添加
      } else {
        scope.selected.push(id);
        // 添加时，超过最大选择数，则剔除上一个选中项
        if (scope.selected.length > options.maxSelection) {
          Utils.eliminate(scope.selected, lastSelected);
        }
        lastSelected = id;
      }
    };

    // 选中当前所有项
    scope.toggleSelectAll = function($event) {
      var ids = Array.prototype.concat.apply([], toArrayFilter(scope.groups));
      ids = ids.map(function(id) {
        return '' + id
      });

      if ($event.target.checked) {
        Utils.concatTo(scope.selected, ids, true);
      } else {
        Utils.removeFrom(scope.selected, ids);
      }
    };

    scope.isSelected = function(id) {
      id = '' + id;
      return scope.selected.indexOf(id) !== -1;
    };

    scope.isSelectAllShow = function() {
      return !options.maxSelection && !isEmptyFilter(scope.groups);
    };

    // 监控选中项，有变化时滚动至最右侧
    scope.$watchCollection('selected', function(val) {
      $ionicScrollDelegate.$getByHandle('us-scroll').scrollBottom(true);
    });

    promise.then(function(modal) {
      var _show = modal.show;

      modal.show = function(_options, callback) {

        _options = _options || {};

        scope.group = {};
        scope.selected = _options.values || [];

        lastSelected = scope.selected[scope.selected.length - 1];

        // 保存，触发回调函数
        scope.save = function() {
          angular.isFunction(callback) && callback.call(modal, scope.selected);
        };
        scope.setGroup(options.groups || {});

        return _show.apply(modal);
      };

      scope.modal = modal;
    });

    return promise;
  }

  function create(options) {
    
    return $$create(angular.extend({
      // groups,
      template: 'templates/user-select-modal.html'
    }, options)).then(function(modal) {

      var scope = modal.scope;
      var offset = 0
      var limit = 20
      var total = 0
      // 用于存放当前部门、岗位下所有用户分组数据
      var userGroups = {}

      // 用于放面包屑
      scope.paths = []
      scope.$watchCollection('paths', function(newPaths, oldPaths) {
        var last = newPaths[newPaths.length - 1]

        offset = 0
        userGroups = getAllUserGroups(last)
        total = Object.keys(userGroups).map(function(key) {
          return userGroups[key].length
        }).reduce(function(a, b) {
          return a + b
        }, 0)
        
        updateDeparmentsAndPositions(last)

        scope.loadMore()
        
        $ionicScrollDelegate.$getByHandle('us-modal').scrollTop()
      })

      function updateDeparmentsAndPositions(conf) {
        var departments = [];
        var positions = [];

        if (conf) {
          // 部门
          if (conf.type === 'department') {
            departments = Department.getSubDepartments(conf.id);
            // 岗位列表
          } else if (conf.type === 'position' && !conf.id) {
            positions = Position.getPositions();
          }
        }

        scope.departments = departments;
        scope.positions = positions;
      }

      /**
       * 获取当前部门、岗位下的所有用户分组
       */
      function getAllUserGroups(conf) {
        var userGroups = User.group;

        if (conf) {
          // 部门
          if (conf.type === 'department') {
            userGroups = Department.getUidGroupByDepartment(conf.id)
            // 岗位
          } else if (conf.type === 'position' && conf.id) {
            userGroups = Position.getUidGroupByPosition(conf.id)
          }
        }

        return userGroups
      }

      /**
       * 将用户分组按一定长度截取出来
       * sliceUserGroup({ 'A': [1, 2, 3], 'B': [4, 5, 6] }, 5)
       *   =>
       * { 'A': [1, 2, 3], 'B': [4, 5] }
       */
      function sliceUserGroup(userGroups, len) {
        var ret = {}
        var letters = Object.keys(userGroups).sort()
        var count = 0

        for(var i = 0; i < letters.length; i++) {
          var letter = letters[i]
          var uids = userGroups[letter]

          if(count + uids.length < len) {
            ret[letter] = [].concat(uids)
            count += uids.length
          } else {
            ret[letter] = [].concat(uids.slice(0, len - count))
            break;
          }
        }

        return ret
      }

      scope.loadMore = function() {
        scope.setGroup(sliceUserGroup(userGroups, offset + limit))
        offset += limit
        scope.$broadcast('scroll.infiniteScrollComplete')
      }

      scope.hasLoadMore = function () {
        return total > offset
      }
      
      scope.back = function() {
        scope.paths.pop();
      };

      // 打开部门
      scope.openDepartment = function(deptId) {
        scope.paths.push({
          type: 'department',
          id: deptId
        });
      };

      // 打开岗位
      scope.openPosition = function(posId) {
        scope.paths.push({
          type: 'position',
          id: posId
        });
      };

      // 弹窗打开时，重置数据
      scope.$on('modal.shown', function() {
        if(!scope.paths.length) {
          scope.loadMore()
        } else {
          scope.paths = [];
        }
      });

      return modal;
    });
  }

  function createPosition(options) {
    return $$create(angular.extend({
      groups: User.positionData.group,
      template: 'templates/user-select-position-modal.html'
    }, options));
  }

  function createDepartment(options) {
    return $$create(angular.extend({
      groups: User.departmentData.group,
      template: 'templates/user-select-department-modal.html'
    }, options));
  }

  return {
    create: create,
    createPosition: createPosition,
    createDepartment: createDepartment
  };
})

.factory('IbPopup', function($timeout, $ionicPopup, $ionicBackdrop) {
  var extend = angular.extend;

  return {
    confirm: function(opts) {
      return $ionicPopup.confirm(extend({
        title: '提示',
        okText: '确定',
        cancelText: '取消'
      }, opts));
    },

    alert: function(opts) {
      return $ionicPopup.alert(extend({
        okText: '确定',
      }, opts));
    },

    prompt: $ionicPopup.prompt,

    show: $ionicPopup.show,

    tip: function(msg, type, timeout) {
      timeout = timeout || 1500;
      type = type || 'info';

      var popup = this.show({
        // title: msg || '',
        cssClass: 'ib-tip ib-tip-' + type,
        content: msg
      });

      $ionicBackdrop.release();

      $timeout(popup.close, timeout);
      return popup;
    },
  };
})

/**
 * @ngdoc service
 * @name ibosApp.services.IbList
 * @description
 *   基本列表构造器
 */
.factory('IbList', function($http, $ionicLoading, Settings) {

  function IbList(url, options) {
    this.url = url;
    this.options = options || {};

    this.hasMore = true;
    this.items = [];

    this.params = {};

    this.idAttr = this.options.idAttr || 'id';
  }

  angular.extend(IbList.prototype, {
    getUrl: function() {
      return Settings.rootUrl + this.url
    },

    parse: function(res) {
      return res;
    },

    showLoading: function() {
      $ionicLoading.show();
    },

    hideLoading: function() {
      $ionicLoading.hide();
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.IbList#fetch
     * @methodOf  ibosApp.services.IbList
     * @description
     *   获取文件列表数据
     *   返回数据格式为
     *   {
     *     datas: [
     *       { id:.. name:... }
     *       ...
     *     ],
     *     hasMore: true
     *   }
     * @param {Obejct} param 查询条件
     *    offset     {Number}         查询起始位置
     * @returns {Object} promise
     */
    fetch: function(params, reset, extend) {
      var _this = this;
      if (reset) {
        this.reset();
      }

      if (extend === false) {
        this.params = params;
      } else {
        angular.extend(this.params, params);
      }


      this.loading = true;
      this.showLoading();

      var req = $http.get(this.getUrl(), {
          params: this.params
        })
        .success(function(res) {
          _this.items = _this.items.concat(_this.parse(res.data || res.datas));
          // 判断是否还能加载更多
          _this.hasMore = res.hasMore;
        });

      req.finally(function() {
        _this.loading = false;
        _this.hideLoading();
      });

      return req
    },

    get: function(id) {
      var ret = null;
      var idAttr = this.idAttr;

      angular.forEach(this.items, function(item) {
        if (item[idAttr] == id) {
          ret = item;
          return false;
        }
      });

      return ret;
    },

    hasItems: function() {
      return !!this.items.length;
    },

    getOffset: function() {
      return this.items.length;
      // return this.items.length ?
      //   this.items[this.items.length - 1][this.idAttr] || '' :
      //   '';
    },

    search: function(keyword) {
      return this.fetch({
        offset: 0,
        search: keyword
      }, true);
    },

    loadMore: function() {
      return this.fetch({
        offset: this.getOffset()
      });
    },

    checkNoData: function() {
      return !this.items.length && !this.hasMore;
    },

    remove: function(id) {
      var _this = this;
      var idAttr = this.idAttr;

      if (!this.items.length) {
        return;
      }

      angular.forEach(this.items, function(item, i) {
        if (item[idAttr] == id) {
          _this.items.splice(i, 1);
          return false;
        }
      });
    },

    reset: function() {
      this.items = [];
      this.hasMore = true;
      this.params = {};
    },

    refresh: function() {
      return this.fetch({
        offset: 0
      }, true);
    }
  });

  return IbList;
})

/**
 * @ngdoc service
 * @name  ibosApp.services.SearchModal
 * @description
 *   搜索 Modal 控件
 */
.factory('SearchModal', function($q, $ionicModal) {
  // 生成搜索功能 modal 实例
  function SearchModal(options) {
    var that = this;

    this.key = this.lastKey = '';
    this.result = [];

    var scope = options.scope;
    var search = angular.isFunction(options.searchFunc) ? options.searchFunc : angular.noop;

    // 向服务器发送搜索请求
    this.send = ionic.debounce(function(key) {
      that.canceler && that.canceler.resolve();

      if (!key) {
        that.reset();
        return;
      }
      // 再次查询的关键字一样时，不处理
      if (key === that.lastKey) return;

      that.canceler = $q.defer();
      that.searching = true;

      search(angular.extend({}, options.defaultParams, {
          key: key
        }), {
          timeout: that.canceler.promise
        }).success(function(res) {
          if (res.isSuccess) {
            that.lastKey = key;
            that.result = res.data;
          }
        })
        .finally(function() {
          that.searching = false;
        })
    }, 200);

    // 重置搜索状态
    this.reset = function() {
      this.searching = false;
      this.lastKey = '';
      this.key = '';
      this.result = [];
    };

    // 打开搜索 modal
    this.showModal = function() {
      if (this.modal) {
        this.modal.show();
      } else {
        // 创建搜索窗口
        $ionicModal.fromTemplateUrl(options.templateUrl, {
          scope: scope,
          animation: 'none'
        }).then(function(modal) {
          modal.hideDelay = 10;
          modal.show();

          that.modal = modal;

          // 关闭搜索窗口时，清空搜索相关数据
          scope.$on('modal.hidden', function() {
            that.reset();
          });

          scope.$on('$ionicView.leave', function() {
            that.modal.hide();
          });

          scope.$on('$destroy', function() {
            that.modal.remove();
          });
        });
      }
    };

    this.hideModal = function() {
      if (this.modal) this.modal.hide();
    };

    this.removeModal = function() {
      if (this.modal) this.modal.remove();
    };

    scope.$watch(function() {
      return that.key
    }, function(key) {
      if (key) that.searching = true;
      that.send(key);
    });
  }

  return SearchModal;
})

/**
 * @ngdoc service
 * @name  ibosApp.services.DefaultBack
 * @description
 *   默认返回行为控制
 */
.factory('DefaultBack', function($ionicHistory, $window, $state, $stateParams, $ionicViewSwitcher) {

    function getDefaultBack() {
      return ($state.current || {}).defaultBack;
    }

    function goDefaultBack() {
      $ionicViewSwitcher.nextDirection('back');
      $ionicHistory.nextViewOptions({
        disableBack: true,
        historyRoot: true
      });

      var params = {};
      var defaultBack = getDefaultBack() || {};

      if (defaultBack.state) {
        if (defaultBack.getStateParams) {
          params = defaultBack.getStateParams($stateParams);
        }
        $state.go(defaultBack.state, params);
      }
    }

    return {
      get: getDefaultBack,
      goBack: function() {
        if ($ionicHistory.backView()) {
          $window.history.back()
            // $ionicHistory.goBack();
        } else {
          goDefaultBack();
        }
      }
    }
  })

/**
 * @ngdoc service
 * @name  ibosApp.services.CommonApi
 * @description
 *   通用接口
 */
.factory('CommonApi', function($http, Settings, Utils) {
  return {
    /**
     * @ngdoc     method
     * @name      ibosApp.services.CommonApi#uploadAttach
     * @methodOf  ibosApp.services.CommonApi
     * @description
     *   上传附件
     * @returns {HttpPromise}  HttpPromise
     */
    uploadAttach: function(data, config) {
      return Utils.postFormData(Settings.hostUrl + '?r=main/upload/upload', data, config)
    },

    /**
     * @ngdoc     method
     * @name      ibosApp.services.CommonApi#uploadBase64Img
     * @methodOf  ibosApp.services.CommonApi
     * @description
     *   上传base64图片数据
     * @returns {HttpPromise}  HttpPromise
     */
    uploadBase64Img: function(base64) {
      return $http.post(Settings.hostUrl + '?r=main/attach/uploadbase', {
        img: base64
      })
    }
  }
})

.service('UploadFile', function(Settings) {
  var This = this;

  this.encodeURI = function(obj) {
    var str = [];
    for (var p in obj) {
      str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p]));
    }
    return str.join('&');
  }

  this.upload = function(opts) {
    var url = opts.url || Settings.hostUrl + '?r=main/upload/upload';
    if (!opts.url && opts.params) {
      url = url + '&' + this.encodeURI(opts.params);
    }
    var xhr = new XMLHttpRequest();
    if (xhr.upload) {
      xhr.upload.addEventListener('progress', function(e) {
        opts.onProgress && opts.onProgress(Math.ceil(e.loaded / e.total * 100));
      }, false);
      xhr.addEventListener('timeout', This.onTimeout, false);
      xhr.onreadystatechange = function(e) {
          if (xhr.readyState === 4) {
            if (xhr.status === 200) {
              opts.onSuccess && opts.onSuccess(xhr.responseText && angular.fromJson(xhr.responseText));
            } else {
              opts.onFail && opts.onFail(xhr.responseText && angular.fromJson(xhr.responseText));
            }
          }
        }
        // 开始上传
      xhr.open('POST', url, true);
      xhr.useXDomain = true;
      xhr.withCredentials = true;
      xhr.setRequestHeader('ISCORS', true);
      xhr.send(opts.data);
    }
    return xhr;
  }
})